package game.dungeon;

import game.square.Direction;

import java.util.ArrayList;
import java.util.List;

import be.kuleuven.cs.som.annotate.Basic;
import be.kuleuven.cs.som.annotate.Raw;

/**
 * A class of coordinates involving a dungeon and x-,y- and z-coordinates.
 * 
 * @Invar		| canHaveAsCoordinates(getXCoordinate(), getYCoordinate(), getZCoordinate())	
 * 
 * @version		1.0
 * @author      Dries Liebens 
 *   			Loic Vercruysse
 */
public class Coordinates {

	/**
	 * Initialize this new Coordinates with 
	 * x-coordinate equal to '1' and y- and z- coordinate equal to '0'.
	 * 
	 * @effect	| this(1,0,0)
	 */
	public Coordinates() {
		this(1,0,0);
	}
	
	/**
	 * Initialize this new Coordinates with coordinates.
	 * 
	 * @param	xCoordinate
	 * 			The x-coordinate to set.
	 * @param	yCoordinate
	 * 			The y-coordinate to set.
	 * @param	zCoordinate
	 * 			The z-coordinate to set.
	 * @post	| new.getDungeon() == dungeon
	 * @post	| new.getXCoordinate() == xCoordinate
	 * @post	| new.getYCoordinate() == yCoordinate
	 * @post	| new.getZCoordinate() == zCoordinate
	 * @throws	IllegalArgumentException
	 * 			| !isValidDungeon(dungeon)
	 * @throws	IllegalArgumentException
	 * 			| !canhaveAsCoordinates(xCoordinate,yCoordinate,zCoordinate)
	 */
	public Coordinates(long xCoordinate, long yCoordinate, long zCoordinate) 
		throws IllegalArgumentException {
		setCoordinates(xCoordinate,yCoordinate,zCoordinate);
	}

	/**
	 * Returns whether this coordinates can have the
	 * given x-,y- and z-coordinates.
	 * 
	 * @param	xCoordinate
	 * 			The x-coordinate to check.
	 * @param	yCoordinate
	 * 			The y-coordinate to check.
	 * @param	zCoordinate
	 * 			The z-coordinate to check.
	 * @return	| if( (xCoordinate < 0) || (xCoordinate > getDungeon().getMaximumXCoordinate()) )
	 * 			|	result == false
	 * 			| else if( (yCoordinate < 0) || (yCoordinate > getDungeon().getMaximumYCoordinate()) 
	 * 			|	result == false
	 * 			| else if( (zCoordinate < 0) || (zCoordinate > getDungeon().getMaximumZCoordinate()) 
	 * 			|	result == false
	 * 			| else
	 * 			|	result == true
	 */
	public boolean canHaveAsCoordinates(long xCoordinate, long yCoordinate, long zCoordinate){
		if(xCoordinate < 0)
			return false;
		if(yCoordinate < 0)
			return false;
		if(zCoordinate < 0)
			return false;
		return true;
	}
	
	/**
	 * Sets the coordinates for this coordinates.
	 * 
	 * @param	xCoordinate
	 * 			The x-coordinate to set.
	 * @param	yCoordinate
	 * 			The y-coordinate to set.
	 * @param	zCoordinate
	 * 			The z-coordinate to set.
	 * @post	| new.getXCoordinate() == xCoordinate
	 * @post	| new.getYCoordinate() == yCoordinate
	 * @post	| new.getZCoordinate() == zCoordinate
	 * @throws	IllegalArgumentException
	 * 			| !canhaveAsCoordinates(xCoordinate,yCoordinate,yCoordinate)
	 */
	public void setCoordinates(long xCoordinate, long yCoordinate, long zCoordinate) {
		if(!canHaveAsCoordinates(xCoordinate,yCoordinate,zCoordinate))
			throw new IllegalArgumentException("Non-valid coordinates!");
		this.xCoordinate = xCoordinate;
		this.yCoordinate = yCoordinate;
		this.zCoordinate = zCoordinate;
	}
	
	/**
	 * Returns the x-coordinate of this coordinates.
	 */
	@Basic @Raw
	public long getXCoordinate() {
		return xCoordinate;
	}
	
	/**
	 * Sets the x-coordinate of this coordinates.
	 * 
	 * @param	xCoordinate
	 * 			The x-coordinate to set.
	 * @throws	IllegalArgumentException
	 * 			| !canHaveAsCoordinates(xCoordinate,getYCoordinate(),getZCoordinate())
	 */
	public void setXCoordinate(long xCoordinate) throws IllegalArgumentException {
		if(!canHaveAsCoordinates(xCoordinate,getYCoordinate(),getZCoordinate()))
			throw new IllegalArgumentException("Non-valid xCoordinate!");
		this.xCoordinate = xCoordinate;
	}
	
	/**
	 * Variable referencing the x-coordinate of this coordinates.
	 */
	private long xCoordinate;
	
	/**
	 * Returns the y-coordinate of this coordinates.
	 */
	@Basic @Raw
	public long getYCoordinate() {
		return yCoordinate;
	}
	
	/**
	 * Sets the y-coordinate of this coordinates.
	 * 
	 * @param	yCoordinate
	 * 			The y-coordinate to set.
	 * @throws	IllegalArgumentException
	 * 			| !canHaveAsCoordinates(getXCoordinate(),yCoordinate,getZCoordinate())
	 */
	public void setYCoordinate(long yCoordinate) throws IllegalArgumentException {
		if(!canHaveAsCoordinates(getXCoordinate(),yCoordinate,getZCoordinate()))
			throw new IllegalArgumentException("Non-valid yCoordinate!");
		this.yCoordinate = yCoordinate;
	}
	
	/**
	 * Variable referencing the y-coordinate of this coordinates.
	 */
	private long yCoordinate;
	
	/**
	 * Returns the z-coordinate of this coordinates.
	 */
	@Basic @Raw
	public long getZCoordinate() {
		return zCoordinate;
	}
	
	/**
	 * Sets the z-coordinate of this coordinates.
	 * 
	 * @param	zCoordinate
	 * 			The z-coordinate to set.
	 * @throws	IllegalArgumentException
	 * 			| !canHaveAsCoordinates(getXCoordinate(),getYCoordinate(),zCoordinate)
	 */
	public void setZCoordinate(long zCoordinate) throws IllegalArgumentException {
		if(!canHaveAsCoordinates(getXCoordinate(),getYCoordinate(),zCoordinate))
			throw new IllegalArgumentException("Non-valid zCoordinate!");
		this.zCoordinate = zCoordinate;
	}
	
	/**
	 * Variable referencing the z-coordinate of this coordinates.
	 */
	private long zCoordinate;	
	
	/**
	 * Returns if given coordinates lays next to this coordinates.
	 * 
	 * @param	coordinates
	 * 			The coordinates to check.
	 * @return	| if(this.equals(coordinates))
	 * 			|	result == false
	 * @return	| let xDifference as Math.abs(getXCoordinate() - coordinates.getXCoordinate())
	 * 			| let yDifference as Math.abs(getYCoordinate() - coordinates.getYCoordinate())
	 * 			| let zDifference as Math.abs(getZCoordinate() - coordinates.getZCoordinate())
	 * 			| if( (xDifference == 0) && (yDifference == 0) && (zDifference == 1) )
	 * 			| 	result == true
	 * 			| if( (xDifference == 0) && (yDifference == 1) && (zDifference == 0) )
	 * 			| 	result == true
	 * 			| if( (xDifference == 1) && (yDifference == 0) && (zDifference == 0) )
	 * 			| 	result == true
	 * 			| else
	 * 			|	result == false
	 */
	public boolean laysNextTo(Coordinates coordinates) {
		if(equals(coordinates))
			return false;
		
		long xDifference = Math.abs(getXCoordinate() - coordinates.getXCoordinate());
		long yDifference = Math.abs(getYCoordinate() - coordinates.getYCoordinate());
		long zDifference = Math.abs(getZCoordinate() - coordinates.getZCoordinate());
		
		if( (xDifference == 0) && (yDifference == 0) && (zDifference == 1) ) {
			return true;
		}
		if( (xDifference == 0) && (yDifference == 1) && (zDifference == 0) ) {
			return true;
		}
		if( (xDifference == 1) && (yDifference == 0) && (zDifference == 0) ) {
			return true;
		}
		
		return false;
	}
	
	/**
	 * Returns is the given object is an instance of the class Coordinates
	 * and has the same values as this coordinates.
	 * 
	 * @return	| if(object == null)
	 * 			|	result == false
	 * @return	| if(object.getClass() != Coordinates.class)
	 * 			|	result = false
	 * @return	| result == ( (coordinates.getXCoordinate() == getXCoordinate())
	 * 			|		&& (coordinates.getYCoordinate() == getYCoordinate())
	 * 			|		&& (coordinates.getZCoordinate() == getZCoordinate()) 
	 * 			|		&& (!coordinates.getDungeon().equals(getDungeon())
	 */
	@Override
	public boolean equals(Object object) {
		if(object == null)
			return false;
		if(object.getClass() != Coordinates.class)
			return false;
		
		Coordinates coordinates = (Coordinates) object;
		if(coordinates.getXCoordinate() != this.getXCoordinate())
			return false;
		if(coordinates.getYCoordinate() != this.getYCoordinate())
			return false;
		if(coordinates.getZCoordinate() != this.getZCoordinate())
			return false;
		return true;
	}
	
	/**
	 * Returns the relative direction of the given coordinates to this coordinates.
	 * 
	 * @param	coordinates
	 * 			The coordinates to get the relative direction from.
	 * @return	| if(this.equals(coordinates))
	 * 			|	result == null
	 * @return	| if(!this.laysNextTo(coordinates))
	 * 			|	result == null
	 * @return	| if( (coordinates.getXCoordinate() == getXCoordinate()) 
	 * 			|			&& (coordinates.getZCoordinate() == getZCoordinate()) 
	 * 			|			&& (coordinates.getYCoordinate() == (getYCoordinate()-1)) )
	 * 			|	result == Direction.SOUTH
	 * @return	| if( (coordinates.getXCoordinate() == getXCoordinate()) 
	 * 			|			&& (coordinates.getZCoordinate() == getZCoordinate()) 
	 * 			|			&& (coordinates.getYCoordinate() == (getYCoordinate()+1)) )
	 * 			|	result == Direction.NORTH
	 * @return	| if( (coordinates.getYCoordinate() == getYCoordinate())
	 * 			|			&& (coordinates.getZCoordinate() == getZCoordinate())
	 * 			|			&& (coordinates.getXCoordinate() == (getXCoordinate()-1)) )
	 * 			|	result == Direction.WEST
	 * @return	| if( (coordinates.getYCoordinate() == getYCoordinate())
	 * 			|			&& (coordinates.getZCoordinate() == getZCoordinate())
	 * 			|			&& (coordinates.getXCoordinate() == (getXCoordinate()+1)) )
	 * 			|	result == Direction.EAST
	 * @return	| if( (coordinates.getXCoordinate() == getXCoordinate()) 
	 * 			|			&& (coordinates.getYCoordinate() == getYCoordinate()) 
	 * 			|			&& (coordinates.getZCoordinate() == (getZCoordinate()-1)) )
	 * 			|	result == Direction.FLOOR 
	 * @return	| if( (coordinates.getXCoordinate() == getXCoordinate()) 
	 * 			|			&& (coordinates.getYCoordinate() == getYCoordinate()) 
	 * 			|			&& (coordinates.getZCoordinate() == (getZCoordinate()+1)) )
	 * 			|	result == Direction.CEILING
	 */
	public Direction getRelativeDirection(Coordinates coordinates) {
		if(equals(coordinates))
			return null;
		if(!laysNextTo(coordinates))
			return null;
		
		if( (coordinates.getXCoordinate() == getXCoordinate())
				&& (coordinates.getZCoordinate() == getZCoordinate()) )
			if(coordinates.getYCoordinate() == (getYCoordinate()-1))
				return Direction.SOUTH;
			else
				return Direction.NORTH;
		
		if( (coordinates.getYCoordinate() == getYCoordinate())
				&& (coordinates.getZCoordinate() == getZCoordinate()) )
			if(coordinates.getXCoordinate() == (getXCoordinate()-1))
				return Direction.WEST;
			else
				return Direction.EAST;
		
		if( (coordinates.getXCoordinate() == getXCoordinate()) 
				&& (coordinates.getYCoordinate() == (getYCoordinate())) )
			if(coordinates.getZCoordinate() == (getZCoordinate()-1))
				return Direction.FLOOR;
			else
				return Direction.CEILING;
		
		return null;
	}
	
	/**
	 * Returns the neighbours this coordinates.
	 * 
	 * @return	| if(getNeighbour(Direction.NORTH) != null)
	 * 			|		result.contains(getNeighbour(Direction.NORTH))
	 * @return	|	if(getNeighbour(Direction.EAST) != null)
	 * 			|		result.contains(getNeighbour(Direction.EAST))
	 * @return	|	if(getNeighbour(Direction.SOUTH) != null)
	 * 			|		result.contains(getNeighbour(Direction.SOUTH))
	 * @return	|	if(getNeighbour(Direction.WEST) != null)
	 * 			|		result.contains(getNeighbour(Direction.WEST))
	 * @return	|	if(getNeighbour(Direction.FLOOR) != null)
	 * 			|		result.contains(getNeighbour(Direction.FLOOR))
	 * @return	|	if(getNeighbour(Direction.CEILING) != null)
	 * 			|		result.contains(getNeighbour(Direction.CEILING))
	 */
	public List<Coordinates> getBorderingCoordinates() {
		List<Coordinates> neighbours = new ArrayList<Coordinates>();

		Coordinates north = getNeighbour(Direction.NORTH);
		Coordinates east = getNeighbour(Direction.EAST);
		Coordinates  south = getNeighbour(Direction.SOUTH);
		Coordinates west = getNeighbour(Direction.WEST);
		Coordinates  ceiling = getNeighbour(Direction.CEILING);
		Coordinates floor = getNeighbour(Direction.FLOOR);
		
		if(north != null)
			neighbours.add(north);
		if(east != null)
			neighbours.add(east);
		if(south != null)
			neighbours.add(south);
		if(west != null)
			neighbours.add(west);
		if(ceiling != null)
			neighbours.add(ceiling);
		if(floor != null)
			neighbours.add(floor);
		
		return neighbours;
	}
	
	/**
	 * Returns the bordering coordinates in given direction.
	 * 
	 * @param	direction
	 * 			The direction in which the neighbour has to be returned.
	 * @return	| if(direction == null)
	 * 			|	result == null
	 * @return	| if( (direction != Direction.NORTH) && (direction != Direction.EAST)
	 * 			|		 && (direction != Direction.SOUTH) && (direction != Direction.WEST)
	 * 			|		 && (direction != Direction.CEILING) && (direction != Direction.FLOOR) )
	 * 			|		result == null
	 * @return	| if( (direction == Direction.NORTH) && canHaveAsCoordinates(getXCoordinate(),getYCoordinate()+1,getZCoordinate()) )
	 * 			| 	result == new Coordinates(getXCoordinate(),getYCoordinate()+1,getZCoordinate())
	 * @return	| if( (direction == Direction.NORTH) && !canHaveAsCoordinates(getXCoordinate(),getYCoordinate()+1,getZCoordinate()) )
	 * 			| 	result == null
	 * @return	| if(( direction == Direction.EAST) && canHaveAsCoordinates(getXCoordinate()+1,getYCoordinate(),getZCoordinate()) )
	 * 			| 	result == new Coordinates(getXCoordinate()+1,getYCoordinate(),getZCoordinate())
	 * @return	| if(( direction == Direction.EAST) && !canHaveAsCoordinates(getXCoordinate()+1,getYCoordinate(),getZCoordinate()) )
	 * 			| 	result == null
	 * @return	| if( (direction == Direction.SOUTH) && canHaveAsCoordinates(getXCoordinate(),getYCoordinate()-1,getZCoordinate()) )
	 * 			| 	result == new Coordinates(getXCoordinate(),getYCoordinate()-1,getZCoordinate())
	 * @return	| if( (direction == Direction.SOUTH) && !canHaveAsCoordinates(getXCoordinate(),getYCoordinate()-1,getZCoordinate()) )
	 * 			| 	result == null
	 * @return	| if( (direction == Direction.WEST) && canHaveAsCoordinates(getXCoordinate()-1,getYCoordinate(),getZCoordinate()) )
	 * 			| 	result == new Coordinates(getXCoordinate()-1,getYCoordinate(),getZCoordinate())
	 * @return	| if( (direction == Direction.WEST) && !canHaveAsCoordinates(getXCoordinate()-1,getYCoordinate(),getZCoordinate()) )
	 * 			| 	result == null
	 * @return	| if( (direction == Direction.CEILING) && canHaveAsCoordinates(getXCoordinate(),getYCoordinate(),getZCoordinate()+1) )
	 * 			| 	result == new Coordinates(getXCoordinate(),getYCoordinate(),getZCoordinate()+1)
	 * @return	| if( (direction == Direction.CEILING) && !canHaveAsCoordinates(getXCoordinate(),getYCoordinate(),getZCoordinate()+1) )
	 * 			| 	result == null
	 * @return	| if( (direction == Direction.FLOOR) && canHaveAsCoordinates(getXCoordinate(),getYCoordinate(),getZCoordinate()-1) )
	 * 			| 	result == new Coordinates(getXCoordinate(),getYCoordinate(),getZCoordinate()-1)
	 * @return	| if( (direction == Direction.FLOOR) && !canHaveAsCoordinates(getXCoordinate(),getYCoordinate(),getZCoordinate()-1) )
	 * 			| 	result == null
	 */
	public Coordinates getNeighbour(Direction direction) {
		if(direction == null)
			return null;
		
		long x = getXCoordinate();
		long y = getYCoordinate();
		long z = getZCoordinate();
		if(direction == Direction.NORTH)
			y++;
		if(direction == Direction.EAST)
			x++;
		if(direction == Direction.SOUTH)
			y--;
		if(direction == Direction.WEST)
			x--;
		if(direction == Direction.CEILING)
			z++;
		if(direction == Direction.FLOOR)
			z--;
		
		try{
			return new Coordinates(x,y,z);
		}
		catch(IllegalArgumentException exc){
			return null;
		}
	}
	
	/**
	 * Returns if there is overlap between the two lines.
	 * All the parameters only can vary in one direction.
	 * 
	 * @param	oneBegin
	 * 			The begin of the first coordinate.
	 * @param	oneEnd
	 * 			The end of the first coordinate.
	 * @param	twoBegin
	 * 			The begin of the second coordinate.
	 * @param	twoEnd
	 * 			The end of the second coordinate.
	 * @return	True if there is an overlap between the two lines.
	 * 			| if( (oneBegin == twoBegin) || (oneBegin == twoEnd) || (twoBegin == oneEnd) || (oneEnd == twoEnd) )
	 *			|	result == true
	 *			| else if( (oneBegin < twoBegin) && (oneEnd > twoBegin) )
	 *			|	result == true
	 *			| else if( (oneBegin < twoEnd) && (oneEnd > twoEnd) )
	 *			|	result == true
	 *			| else if( (twoBegin < oneBegin) && (twoEnd > oneBegin) )
	 *			|	result == true
	 *			| else if( (twoBegin < oneEnd) && (twoEnd > oneEnd) )
	 *			|	result == true
	 *			| else if( (oneBegin > twoBegin) && (oneEnd < twoEnd) )
	 *			|	result ==true
	 *			| else if( (twoBegin > oneBegin) && (twoEnd < oneEnd) )
	 *			|	result == true	
	 *			| else
	 *			|	result == false
	 */
	public static boolean hasOverlap(long oneBegin, long oneEnd, long twoBegin, long twoEnd) {
		if( (oneBegin == twoBegin) || (oneBegin == twoEnd) || (twoBegin == oneEnd) || (oneEnd == twoEnd) )
			return true;
		if( (oneBegin < twoBegin) && (oneEnd > twoBegin) )
			return true;
		if( (oneBegin < twoEnd) && (oneEnd > twoEnd) )
			return true;
		if( (twoBegin < oneBegin) && (twoEnd > oneBegin) )
			return true;
		if( (twoBegin < oneEnd) && (twoEnd > oneEnd) )
			return true;
		if( (oneBegin > twoBegin) && (oneEnd < twoEnd) )
			return true;
		if( (twoBegin > oneBegin) && (twoEnd < oneEnd) )
			return true;		

		return false;
	}
	
	/**
	 * Returns the hashcode of this object.
	 * 
	 * @return	| result == (getXCoordinate()-getYCoordinate())/2 + getZCoordinate()/4).intValue()
	 */
	@Override
	public int hashCode() {
		Long hash = (getXCoordinate()-getYCoordinate())/2 + getZCoordinate()/4;
		return hash.intValue();
	}
	
	/**
	 * Returns a string-representation of this object
	 * 
	 * @return	A string-representation of this object.
	 * 			| result == {getXCoordinate(),getYCoordinate(),getZCoordinate()}
	 */
	@Override
	public String toString() {
		return "{" + getXCoordinate() + "," + getYCoordinate() + "," + getZCoordinate() + "}";
	}
}